package org.text2diagram.ddl.converter;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import org.text2diagram.ddl.domain.Appearance;
import org.text2diagram.ddl.domain.Block;
import org.text2diagram.ddl.domain.BlockStyle;
import org.text2diagram.ddl.domain.Color;
import org.text2diagram.ddl.domain.Container;
import org.text2diagram.ddl.domain.Group;
import org.text2diagram.ddl.domain.Layout;
import org.text2diagram.ddl.domain.Link;
import org.text2diagram.ddl.domain.LinkKey;
import org.text2diagram.ddl.domain.Link.LinkDirection;
import org.text2diagram.ddl.domain.Link.LinkStyle;

public class PlainTextConverter {

	private static final String LAYOUT_AS_KEYWORD = "layout as ";
	private static final String STYLE_IS_KEYWORD = " style is ";
	private static final String APPEARANCE_IS_KEYWORD = " appearance is ";
	private static final String BLOCK_LIST_DELIMITER = ",";
	private static final String COLOR_IS_KEYWORD = " color is ";
	private static final Pattern STYLE_IS_REGEXP = Pattern.compile("style (.*) is (.*) appearance and (.*) color");
	private static final Pattern GROUP_INTO_REGEXP = Pattern.compile("group (.*?) into (.*?)");

	private HashMap<String, Block> blockMap = new HashMap<String, Block>();
	private HashMap<LinkKey, Link> linkMap = new HashMap<LinkKey, Link>();
	private HashMap<String, BlockStyle> styleMap = new HashMap<String, BlockStyle>();
	private ArrayList<Group> groupList = new ArrayList<Group>();
	private ArrayList<Container> containerList = new ArrayList<Container>();
	private Layout layout;	
	
	private ArrayList<ConverterException> parsingErrors;
	
	private enum ExpressionType {
		LINK,
		GROUP,
		BLOCKS_WITHIN_BLOCK,
		BLOCK_CONTAINS_BLOCKS,
		APPEARANCE,
		COLOR,
		STYLE_DESCRIPTION,
		STYLE,
		LAYOUT
	}
	
	public static DiagramDescription convertDescription(String text) throws ConverterException {
		PlainTextConverter converter = new PlainTextConverter();
		String[] lines = text.split("\n");
		for (int lineNo=0; lineNo < lines.length; lineNo++) {
			String line = lines[lineNo];
			ExpressionType expType = converter.guessExpressionType(line);
			if (expType == null) //unknown expression
				throw new ConverterException("Unknown expression \"" + line+ "\"", lineNo);
			
			try {
				switch (expType) {
					case LINK: 
						converter.parseLink(line); 
						break;
					case GROUP:
						converter.parseGroup(line);
						break;
					case BLOCKS_WITHIN_BLOCK:
						//TODO: implement parseBlocksWithinBlock
						break;
					case BLOCK_CONTAINS_BLOCKS:
						//TODO: implement parseBlockContainsBlocks
						break;
					case APPEARANCE:
						converter.parseAppearance(line);
						break;
					case COLOR:
						converter.parseColor(line);
						break;
					case STYLE_DESCRIPTION:
						converter.parseStyleDescription(line);
						break;
					case STYLE:
						converter.parseStyle(line);
						break;
					case LAYOUT:
						converter.parseLayout(line);
						break;
				}
			} catch (ConverterException ce) {
				ce.setLineNo(lineNo);
				converter.parsingErrors.add(ce);
			}			
		}
		
		converter.assignGroupsToLinks();
		
		DiagramDescription diagramDescription = new DiagramDescription();
		diagramDescription.getBlockList().addAll(converter.blockMap.values());
		diagramDescription.getLinkList().addAll(converter.linkMap.values());
		diagramDescription.setContainerList(converter.containerList);
		diagramDescription.setGroupList(converter.groupList);
		diagramDescription.setLayout(converter.layout);
		
		return diagramDescription;
	}
	
	private void parseGroup(String exp) throws ConverterException {
		Matcher matcher = GROUP_INTO_REGEXP.matcher(exp);
		matcher.matches();
		String blockNames = matcher.group(1);
		String groupName = matcher.group(2);
		
		ArrayList<Block> blocks = findOrCreateBlocks(blockNames);
		Group group = new Group(groupName, blocks);
		groupList.add(group);
	}	
	
	private void assignGroupsToLinks() {
		for (Link link : linkMap.values()) {
			//search for group which contains both source and target blocks
			for (Group group : groupList) {
				if (group.getBlocks().contains(link.getSource()) &&
					group.getBlocks().contains(link.getTarget())) {
						link.setGroup(group);
						group.getLinks().add(link);
						break;
				}
			}
		}
	}
	
	private void parseLink(String exp) {
		LinkDirection direction = null;
		LinkStyle style = null;
		String linkExp = null;
		if (exp.contains("->")) {
			direction = LinkDirection.DIRECT;
			style = LinkStyle.SOLID;
			linkExp = "->";
		}
		if (exp.contains("<-")) {
			direction = LinkDirection.REVERSE;
			style = LinkStyle.SOLID;
			linkExp = "<-";
		}
		if (exp.contains("<->")) {
			direction = LinkDirection.BIDIRECT;
			style = LinkStyle.SOLID;
			linkExp = "<->";
		}
		if (exp.contains("-->")) {
			direction = LinkDirection.DIRECT;
			style = LinkStyle.DASHED;
			linkExp = "-->";
		}
		if (exp.contains("<--")) {
			direction = LinkDirection.REVERSE;
			style = LinkStyle.DASHED;
			linkExp = "<--";
		}
		if (exp.contains("<-->")) {
			direction = LinkDirection.BIDIRECT;
			style = LinkStyle.DASHED;
			linkExp = "<-->";
		}
		
		//find source block
		int indexOfLinkExpStart = exp.indexOf(linkExp);
		String sourceBlockName = exp.substring(0, indexOfLinkExpStart).trim();
		int indexOfLinkDescriptionStart = exp.indexOf(":");
		int indexOfTargetStart = indexOfLinkExpStart + linkExp.length();
		String targetBlockName = exp.substring(indexOfTargetStart, indexOfLinkDescriptionStart).trim();
		String linkDescription = exp.substring(indexOfLinkDescriptionStart + 1).trim();
		
		Block sourceBlock = findOrCreateBlock(sourceBlockName);
		Block targetBlock = findOrCreateBlock(targetBlockName);
		findOrCreateLink(sourceBlock, targetBlock, direction, style, linkDescription);
	}
	
	private Block findOrCreateBlock(String name) {
		Block block = blockMap.get(name);
		if (block == null) {
			block = new Block(name);
			blockMap.put(name, block);
		}
		return block;
	}
	
	private ArrayList<Block> findOrCreateBlocks(String namesList) {
		ArrayList<Block> result = new ArrayList<Block>();
		String[] names = namesList.split(BLOCK_LIST_DELIMITER);
		for (String name : names) {
			Block block = findOrCreateBlock(name.trim());
			result.add(block);
		}
		return result;
	}
	
	private Link findOrCreateLink(Block source, Block target, LinkDirection direction, LinkStyle style, String description) {
		LinkKey linkKey = new LinkKey(description, source, target);
		Link link = linkMap.get(linkKey);
		if (link == null) {
			link = new Link(source, target, direction, style, description);
			linkMap.put(linkKey, link);
		}
		return link;
	}	

	private void parseColor(String exp) throws ConverterException {
		int indexOfKeyword = exp.indexOf(COLOR_IS_KEYWORD);
		String blockNamesList = exp.substring(0, indexOfKeyword);
		String colorDescription = exp.substring(indexOfKeyword + COLOR_IS_KEYWORD.length());
		
		ArrayList<Block> blocks = findOrCreateBlocks(blockNamesList);
		Color color = Color.findColor(colorDescription);
		for (Block block : blocks)
			block.setColor(color);
	}	
	
	private void parseAppearance(String exp) throws ConverterException {
		int indexOfKeyword = exp.indexOf(APPEARANCE_IS_KEYWORD);
		String blockNamesList = exp.substring(0, indexOfKeyword);
		String appearanceDescription = exp.substring(indexOfKeyword + APPEARANCE_IS_KEYWORD.length());
		
		ArrayList<Block> blocks = findOrCreateBlocks(blockNamesList);
		Appearance appearance = Appearance.findAppearance(appearanceDescription);
		for (Block block : blocks)
			block.setAppearance(appearance);
	}	
	
	private void parseStyleDescription(String exp) throws ConverterException {
		Matcher matcher = STYLE_IS_REGEXP.matcher(exp);
		String styleName = matcher.group(1);
		String appearanceName = matcher.group(2);
		String colorName = matcher.group(3);
		
		Color color = Color.findColor(colorName);
		Appearance appearance = Appearance.findAppearance(appearanceName);
		BlockStyle blockStyle = new BlockStyle(styleName, appearance, color);
		styleMap.put(styleName, blockStyle);
	}	
	
	private void parseStyle(String exp) throws ConverterException {
		int indexOfKeyword = exp.indexOf(STYLE_IS_KEYWORD);
		String blockNamesList = exp.substring(0, indexOfKeyword);
		String styleName = exp.substring(indexOfKeyword + STYLE_IS_KEYWORD.length());
		BlockStyle blockStyle = styleMap.get(styleName);
		
		//apply style
		ArrayList<Block> blocks = findOrCreateBlocks(blockNamesList);
		for (Block block : blocks) {
			block.setAppearance(blockStyle.getAppearance());
			block.setColor(blockStyle.getColor());
		}
	}
	
	private void parseLayout(String exp) throws ConverterException {
		String layoutName = exp.substring(LAYOUT_AS_KEYWORD.length()).trim();
		Layout layout = Layout.valueOf(layoutName);
		this.layout = layout;
	}		

	private ExpressionType guessExpressionType(String exp) {
		if (exp.contains("->") || exp.contains("<-") || exp.contains("<->") || 
				exp.contains("-->") || exp.contains("<--") || exp.contains("<-->"))
			return ExpressionType.LINK;
		
		if (exp.contains(" within ") || (exp.startsWith("group ") && exp.contains(" together into ")))
			return ExpressionType.GROUP;
		
		if (exp.contains(APPEARANCE_IS_KEYWORD)) 
			return ExpressionType.APPEARANCE;
			
		if (exp.contains(COLOR_IS_KEYWORD))
			return ExpressionType.COLOR;
		
		if (exp.contains(" style ") && exp.contains(" is ") && exp.contains(" appearance and ") && exp.contains(" color"))
			return ExpressionType.STYLE_DESCRIPTION;

		if (exp.contains(STYLE_IS_KEYWORD))
			return ExpressionType.STYLE;

		if (exp.contains(LAYOUT_AS_KEYWORD))
			return ExpressionType.LAYOUT;
		
		if (exp.matches(GROUP_INTO_REGEXP.pattern()))
			return ExpressionType.GROUP;
				
		return null;
	}

	public void setLayout(Layout layout) {
		this.layout = layout;
	}

	public Layout getLayout() {
		return layout;
	}

	public void setParsingErrors(ArrayList<ConverterException> parsingErrors) {
		this.parsingErrors = parsingErrors;
	}

	public ArrayList<ConverterException> getParsingErrors() {
		return parsingErrors;
	}
	
}
